2. Claims-Based Authorization
Along with being able to be
integrated into Windows authentication, WCF also supports a
claims-based technique for authorization. Before going into the
mechanics of claims-based authorization, a couple of moments defining
the terminology are helpful.
A claim
describes an individual right or action that is applicable to a
specific resource. It is defined by a combination of three pieces of
information: the type of claim, the right that is being claimed, and
the resource to which the claim applies. As an example, an identity claim might consist of an identity claim type, a possess propertyusername
resource. This claim might be used to represent the identity associated
with the requester (as indicated by the username resource) in a WCF
communication. It is not difficult to imagine that the client could
make claims of other types to represent concepts such as public keys,
e-mail addresses, roles, and so on. right, and a
A claim set (or ClaimSet)
is an ordered list of the claims that come from a common issuer. For
example, a number of claims can be made based on the use of an X.509
certificate. This includes the common name, the DNS name, the public
key, and the e-mail address. Although each of these items is an
individual claim, the fact that there is a common issuer for the claims
means that the claims can be considered as a single group. This
abstraction allows a group of claims to be applied or rejected en masse.
An authorization policy
is an extensibility point by which new claim sets can be added to the
current WCF calling context. There are two sources (in WCF) for
authorization policies. First, these policies are created based on the
security tokens supplied by the requester. The idea with
these policies is to provide a common representation for tokens that
are generated by heterogeneous sources. The tokens can come from X.509,
Kerberos, or username/password combinations. The authorization policy
permits a representation that is independent of the source for the data.
The second type of
authorization policy comes from configuration details set by an
administrator. The goal here is to add claims to the calling context
for use with authorization by the service. The types of claims that fit
into this category are ones related to local security policy and
information. For example, perhaps a service-side database maps
requesters to roles. This list of roles is a claim set that is not
known to the client but is instead added to the request when it reaches
the service.
As part of the authorization process, the service authorization manager
is the final authority for deciding whether a particular requester
should be granted or denied access to a service. The manager is called
for each request that arrives at the service. It examines the operation
that is being called, along with any available evidence about the
caller (found in the AuthorizationContext object). The AuthorizationContext
contains all the claim sets that are created based on the various
authorization policies, so at the point at which the manager makes its
decision, it has access to any claims regarding identity, public keys,
e-mail addresses, and so on. After this information has been digested,
the service authorization manager makes a go/no go decision for the
incoming request.
Note:
Although the claims-based authorization system is quite powerful, it is
not a replacement for the common language runtime (CLR) authorization
model that is built into the .NET Framework. Indeed, in many instances,
the claims-based authorization mechanism will make changes to the
principal that is available to the current thread by adding or removing
roles to which the current user belongs.
2.1. Accessing the Claims
Now that you know the
fundamental terminology, turn your focus to how the claims are used.
One of the classes at the heart of claims-based authorization is the ServiceSecurityContext. Through this class, it is possible to gain access to the AuthorizationContext (which contains the claim sets), the authorization policies, and the identity claim.
Note: If no security is being applied by WCF, the ServiceSecurityContext object will be null. Even if the caller’s identity is anonymous, this does not guarantee that the ServiceSecurityContext will be instantiated and populated. However, an IsAnonymous property on the ServiceSecurityContext object can be used to determine whether an anonymous identity was used to build the security context object.
The ServiceSecurityContext is exposed through a static property on the OperationContext. You can retrieve the ServiceSecurityContext as shown here:
' VB
Dim securityContext As ServiceSecurityContext = _
OperationContext.Current.ServiceSecurityContext
// C#
ServiceSecurityContext securityContext =
OperationContext.Current.ServiceSecurityContext;
After the ServiceSecurityContext
object is retrieved, you can examine it to find out the go/no go
decision based on the claim sets. For example, the following code would
check to see whether an e-mail address has been included in any of the
claims.
' VB
Dim email As String = String.Empty
Dim claims as IEnumerable(Of Claim) = _
securityContext.AuthorizationContext.ClaimSets(0).FindClaims _
(ClaimTypes.Email, Rights.PossessProperty)
Dim c As Claim
For Each c in claims
email = TryCast(c.Resource, String)
Next
If String.IsNullOrEmpty(email) Then
Throw New SecurityException("Email address claim not found.")
End If
// C#
string email = String.Empty;
IEnumerable<Claim> claims =
securityContext.AuthorizationContext.ClaimSets[0].FindClaims
(ClaimTypes.Email,Rights.PossessProperty);
foreach (Claim c in claims)
{
email = c.Resource as string;
}
if (String.IsNullOrEmpty(email))
throw new SecurityException("Email address claim not found.");
The preceding code uses a
couple of concepts that are worth consideration. First, notice that the
claim sets associated with the request are exposed through the ClaimSets property in the AuthorizationContext object. This collection (the ClaimSets
property) contains all the claims associated with the request. You can
find the details about any of the claims generated either by the processing of the incoming security tokens or through administrator-configured settings in this collection.
The ClaimSet class has a method called FindClaims. This method retrieves an IEnumerable list of Claim
objects that meet a specific set of criteria. The specified criteria
include the type of claim and the rights of the claim. After the Claim objects have been retrieved, it becomes a simple matter to iterate over them to perform any authorization logic required.
Although you might get the impression from looking at the call to FindClaims
that the type and rights are enumerated values, this is incorrect.
Instead, these values are strings that represent a uniform resource
indicator (URI) for the claim type and right. There is a ClaimTypes class and a Rights class that have a number of static properties containing URIs for common types and rights. Table 1 and Table 2 list the types and rights in these classes.
Table 1. Properties on the ClaimTypes Class Representing Claim URIs
Name | Description |
---|
Anonymous | The anonymous user claim. |
Authentication | Indicates whether an identity is authenticated. |
AuthorizationDecision | Specifies the authorization decision on an entity. |
Country | Indicates the country region in which an entity resides. |
DateOfBirth | Specifies the entity’s date of birth. |
DenyOnlySid | Indicates a deny-only security identifier (SID) for an entity. |
Dns | Specifies
the DNS name associated with the computer name or with the alternative
name of either the subject or issuer of an X.509 certificate. |
Email | Indicates the e-mail address of an entity. |
Gender | Specifies the gender of an entity. |
GivenName | Represents the given name of an entity. This is typically the first name of the person represented by the entity. |
Hash | Specifies a hash value for the entity. |
HomePhone | Indicates the home phone number of an entity. |
Locality | Specifies the locale in which an entity resides. |
MobilePhone | Indicates the mobile phone number of an entity. |
Name | Specifies the name of an entity. |
NameIdentifier | Specifies an alternative name for an entity. |
OtherPhone | Indicates the alternative phone number of an entity. |
PostalCode | Gets the URI for a claim that specifies the postal code of an entity. |
PPID | Gets the URI for a claim that specifies the private personal identifier (PPI) of an entity. |
Rsa | Gets the URI for a claim that specifies an RSA key. |
Sid | Gets the URI for a claim that specifies an SID. |
Spn | Gets the URI for a claim that specifies a service principal name (SPN) claim. |
StateOrProvince | Gets the URI for a claim that specifies the state or province in which an entity resides. |
StreetAddress | Gets the URI for a claim that specifies the street address of an entity. |
Surname | Gets
the URI for a claim that specifies the surname of an entity. This would
typically be the last name of a person represented by the entity. |
System | Gets the URI for a claim that identifies the system entity. |
Thumbprint | Gets the URI for a claim that specifies a thumbprint. |
Upn | Gets the URI for a claim that specifies a user principal name (UPN). |
Uri | Gets the URI for a claim that specifies a URI. |
Webpage | Gets the URI for a claim that specifies the Web page of an entity. |
X509DistinguishedName | Gets the string that contains the URI for a distinguished name claim of an X.509 certificate. |
Table 2. Properties on the Rights Class Representing Right URIs
Name | Description |
---|
Identity | Gets a string that specifies that the right represents an identity |
PossessProperty | Gets a string that specifies that the right represents a property that the entity associated with a claim possesses |
The code from the previous
example (which determined whether any claim represented an email
address) is intended to be included in the logic for a service
operation. However, that is not necessarily the best place to be
performing this type of logic. In many cases, the desire to reuse
authorization logic or to decouple the logic from the operations would
lead to a more independent solution. In this case, the logic would be a
custom authorization policy.
To implement a custom authorization policy, start by creating a class that implements the IAuthorizationPolicy interface. The interface itself is fairly straightforward. There are two properties: Id and Issuer. The Id
property is a unique identifier for the authorization component (which,
in this case, is the instance of the custom policy class). The Issuer property is a ClaimSet
that represents the entity that issued this policy. In both cases,
these are read-only properties. From an implementation perspective,
this means that the backing values for these properties should be set
in the constructor and returned in the property Get
function. This is demonstrated in the following code. Please be aware
that this example is not, by itself, a full implementation of the CustomPolicy class. Specifically, the Evaluate method that is part of the IAuthorizationPolicy interface is covered later in this section.
' VB
Public Class CustomPolicy
Implements IAuthorizationPolicy
Private _id As String
Private _issuer As ClaimSet
Public Sub New()
_id = Guid.NewGuid().ToString()
_issuer = ClaimSet.System
End Sub
Public ReadOnly Property Id() As String _
Implements IAuthorizationPolicy.Id
Get
Return _id
End Get
End Property
Public ReadOnly Property Issuer() As ClaimSet _
Implements IAuthorizationPolicy.Issuer
Get
Return _issuer
End Get
End Property
End Class
// C#
public class CustomPolicy : IAuthorizationPolicy
{
private string id;
private ClaimSet issuer;
public CustomPolicy()
{
id = Guid.NewGuid().ToString();
issuer = ClaimSet.System;
}
public string Id
{
get { return id; }
}
public ClaimSet Issuer
{
get { return issuer; }
}
}
In the constructor, the value of the Issuer property is set to the ClaimSet.System
value. This value is used if the current application is the issuer of
the claim. Technically, it indicates an application-trusted issuer
without needing to provide additional details about the issuer.
However, conventionally, it is used when the current application (or
something that has been configured within the current application’s
configuration file) is issuing the claim.
The only method that appears in the IAuthorizationPolicy interface is called Evaluate.
This is where the majority of the work associated with the custom
authorization policy takes place. The signature of the method includes
an EvaluationContext and a state object.
' VB
Public Function Evaluate(ByVal context As EvaluationContext, _
ByRef state As Object) As Boolean _
Implements IAuthorizationPolicy.Evaluate
// C#
public bool Evaluate(EvaluationContext context, ref object state)
The EvaluationContext
object represents the results of an authorization policy doing its
work. If claims are generated as part of an authorization policy, these
claims are added to the evaluation context. The state object is simply an object that is passed into every invocation of the Evaluate method for a particular authorization policy. The actual method signature marks the state object as being passed by reference. This means that that method can create a new object and assign it to the state
parameter. In practice, this parameter is frequently used as a cache
for previously created claims or to ensure that claims are added only
once.
After the authorization
policy has been created, it must be associated with the WCF
authorization process. You can do this either imperatively or
declaratively. When you do so imperatively, there is a little more work
than normal. Start by creating a List of IAuthorizationPolicy objects. After all the policies have been added to the list, a read-only version of the list is assigned to the ExternalAuthorizationPolicies property on the Authorization property for the service host. The following code demonstrates this technique:
' VB
Dim policies As List(Of IAuthorizationPolicy) = _
New List(Of IAuthorizationPolicy)()
policies.Add(New CustomPolicy())
Dim host As New ServiceHost(GetType(TestService))
host.Authorization.ExternalAuthorizationPolicies = _
policies.AsReadOnly()
// C#
List<IAuthorizationPolicy> policies = new List<IAuthorizationPolicy>();
policies.Add(new CustomPolicy());
ServiceHost host = new ServiceHost(typeof(TestService));
host.Authorization.ExternalAuthorizationPolicies =
policies.AsReadOnly();
The declarative technique is a little easier. You can place an authorizationPolicies element within the serviceAuthorization element for a service’s behavior. The following segment from a configuration file demonstrates adding the CustomPolicy authorization policy to the service.
<behavior name="DemoBehavior">
<serviceAuthorization>
<authorizationPolicies>
<add policyType="DemoLibrary.CustomPolicy" />
</authorizationPolicies>
</serviceAuthorization>
</behavior>